package com.ushine.web.communication.netty.file.impl;

import com.google.protobuf.ByteString;
import com.ushine.web.communication.netty.NettySocketServer;
import com.ushine.web.communication.netty.config.NettyConfig;
import com.ushine.web.communication.netty.file.IFileHandlerService;
import com.ushine.web.communication.netty.proto.DataFileMessage;
import com.ushine.web.communication.netty.proto.Message;
import com.ushine.web.component.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Author: yls
 * @Date 2022/5/11 12:34
 * @Description: 文件处理器
 * @Version 1.0
 **/
@Slf4j
@Service
public class FileHandlerServiceImpl implements IFileHandlerService {

    private final Map<String, Object> control = new ConcurrentHashMap<>();

    @Resource
    private NettySocketServer nettySocketServer;

    @Resource
    private NettyConfig nettyConfig;

    @Override
    public boolean filePush(String serialNumber, Path path, long version, String fileNameType) {
        String fileName = path.toFile().getName();
        Path sliceFile = Paths.get(nettyConfig.getPath(), fileName);
        //判断文件是否已经切片
        if (Files.exists(sliceFile)) {
            //效验文件是否正在切片
            if (control.containsKey(fileName)) {
                return false;
            }
            return sendAllFile(sliceFile, serialNumber, version, fileNameType);
        } else {
            synchronized (FileHandlerServiceImpl.class) {
                //判断文件是否已经切片
                if (Files.exists(sliceFile)) {
                    //效验文件是否正在切片
                    if (control.containsKey(fileName)) {
                        return false;
                    }
                    return sendAllFile(sliceFile, serialNumber, version, fileNameType);
                } else {
                    //文件未切片,文件进行切片
                    if (slice(path)) {
                        return sendAllFile(sliceFile, serialNumber, version, fileNameType);
                    } else {
                        return false;
                    }
                }
            }
        }
    }

    @Override
    public boolean filePull(String serialNumber, String fileName, int currentChunk, long version, String fileNameType) {
        Path path = Paths.get(nettyConfig.getPath(), fileName);
        try {
            File[] files = path.toFile().listFiles();
            if (files == null) {
                return false;
            }
            String mainFileMD5 = getMainFileMD5(files);
            //验证主文件MD5是否丢失,如果丢失就上传分片文件
            if (mainFileMD5 == null) {
                log.warn("主文件的MD5丢失,文件路径:" + path);
                Files.delete(path);
                return false;
            }
            //获取总分片数量
            int maxChunk = files.length - 1;
            String currentChunkName = String.valueOf(currentChunk);
            for (File file : files) {
                String[] str = file.getName().split("_");
                if (currentChunkName.equals(str[0])) {
                    sendData(file, serialNumber, version, mainFileMD5, maxChunk, fileName, DataFileMessage.DataFile.Type.PULL, fileNameType);
                    return true;
                }
            }
        } catch (IOException | ServiceException e) {
            log.error("发送文件出现异常,序列号:" + serialNumber + ",文件名称：" + path + ",分片:" + currentChunk, e);
            return false;
        }
        return true;
    }

    @Override
    public boolean deleteFileChunk(String fileName) throws IOException {
        return Files.deleteIfExists(Paths.get(nettyConfig.getPath(), fileName));
    }

    /**
     * 功能描述:
     * 〈全量发送切片文件〉
     *
     * @param sliceFile    切片文件
     * @param serialNumber 序列号
     * @param version      请求版本
     * @param fileNameType 文件类型
     * @return : boolean true:发送成功,false:发送失败
     * @author : yls
     * @date : 2022/5/11 12:38
     */
    private boolean sendAllFile(Path sliceFile, String serialNumber, long version, String fileNameType) {
        try {
            File file = sliceFile.toFile();
            File[] files = file.listFiles();
            String mainFileMD5;
            if (files != null) {
                mainFileMD5 = getMainFileMD5(files);
                //验证主文件MD5是否丢失,如果丢失就上传分片文件
                if (mainFileMD5 == null) {
                    log.warn("主文件的MD5丢失,文件路径:" + sliceFile);
                    Files.delete(sliceFile);
                    return false;
                }
                //获取总分片数量
                int maxChunk = files.length - 1;
                for (File f : files) {
                    if (!f.getName().startsWith("md5")) {
                        sendData(f, serialNumber, version, mainFileMD5, maxChunk, file.getName(),DataFileMessage.DataFile.Type.PUSH, fileNameType);
                    }
                }
            }
            return true;
        } catch (RuntimeException | IOException | ServiceException e) {
            log.error("发送文件出现异常,序列号:{},文件名称：{}", serialNumber, sliceFile, e);
            return false;
        }
    }


    /**
     * 功能描述:
     * 〈文件切片〉
     *
     * @param path 文件路径
     * @return : boolean true:切片成功,false:切片异常
     * @author : yls
     * @date : 2022/5/11 12:52
     */
    private boolean slice(Path path) {
        String fileName = path.toFile().getName();
        try (
                BufferedInputStream bufferedInputStream = new BufferedInputStream(Files.newInputStream(path, StandardOpenOption.READ))
        ) {
            control.put(fileName, "");
            //创建切片文件夹
            Files.createDirectories(Paths.get(nettyConfig.getPath(), fileName));
            MessageDigest digest = MessageDigest.getInstance("MD5");
            byte[] bytes = new byte[nettyConfig.getCheckSize()];
            int len;
            //分片
            int chunk = 0;
            while ((len = bufferedInputStream.read(bytes)) != -1) {
                digest.update(bytes, 0, len);
                byte[] data = new byte[len];
                System.arraycopy(bytes, 0, data, 0, len);
                writeChunk(data, fileName, chunk);
                chunk++;
            }
            String fileMD5 = repairMD5(new BigInteger(1, digest.digest()).toString(16));
            Path mainFile = Paths.get(nettyConfig.getPath(), fileName, "md5_" + fileMD5);
            Files.createFile(mainFile);
        } catch (IOException | NoSuchAlgorithmException e) {
            log.error("文件切片出现异常!", e);
            return false;
        } finally {
            control.remove(fileName);
        }
        return true;
    }

    /**
     * 功能描述:
     * 〈写出分片〉
     *
     * @param data     分片数据
     * @param fileName 主文件名称
     * @param chunk    切片编号
     * @return : void
     * @author : yls
     * @date : 2022/5/11 12:49
     */
    private void writeChunk(byte[] data, String fileName, int chunk) throws NoSuchAlgorithmException, IOException {
        MessageDigest digest = MessageDigest.getInstance("MD5");
        digest.update(data, 0, data.length);
        String chunkMD5 = repairMD5(new BigInteger(1, digest.digest()).toString(16));
        Path path = Paths.get(nettyConfig.getPath(), fileName, chunk + "_" + chunkMD5);
        Files.write(path, data, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
    }

    /**
     * 功能描述:
     * 〈MD5效验码修复：十六进制就是0e，转化成字符串会忽略掉前导零。〉
     *
     * @param md5 原始MD5效验码
     * @return : java.lang.String 修复后数据
     * @author : yls
     * @date : 2022/5/11 12:49
     */
    private String repairMD5(String md5) {
        if (md5.length() < 32) {
            StringBuilder md5Builder = new StringBuilder(md5);
            for (int i = md5Builder.length(); i < 32; i++) {
                md5Builder.insert(0, '0');
            }
            return md5Builder.toString();
        } else {
            return md5;
        }
    }


    /**
     * 功能描述:
     * 〈获取主文件MD5值〉
     *
     * @param files 切片文件夹
     * @return : java.lang.String 主文件MD5值
     * @author : yls
     * @date : 2022/5/11 12:50
     */
    private static String getMainFileMD5(File[] files) {
        //获取主文件MD5值
        for (File f : files) {
            if (f.getName().startsWith("md5")) {
                return f.getName().split("_")[1];
            }
        }
        return null;
    }


    /**
     * 功能描述:
     * 〈 数据发送 〉
     *
     * @param file 发送数据包
     * @param serialNumber 序列号
     * @param version 请求版本
     * @param mainFileMD5 主文件MD5值
     * @param maxChunk 最大分片数
     * @param fileName 文件名称
     * @param type 类型
     * @param fileNameType 文件格式名称
     * @return : void
     * @author : yls
     * @date : 2022/5/11 12:50
     */
    private void sendData(File file, String serialNumber, long version,
                          String mainFileMD5, int maxChunk, String fileName,
                          DataFileMessage.DataFile.Type type, String fileNameType)
            throws IOException, ServiceException {
        String[] filenames = file.getName().split("_");
        byte[] data = Files.readAllBytes(file.toPath());
        ByteString.Output output = ByteString.newOutput();
        output.write(data);
        Message.Instruction instruction = Message.Instruction.newBuilder()
                .setType(Message.Instruction.Type.FILEDATA)
                .setDatafile(DataFileMessage.DataFile
                        .newBuilder()
                        .setData(output.toByteString())
                        .setType(type)
                        .setMaxChunk(maxChunk)
                        .setCurrentChunk(Integer.parseInt(filenames[0]))
                        .setFileMD5(mainFileMD5)
                        .setChunkMD5(filenames[1])
                        .setFilename(fileName)
                        .setFilenameType(fileNameType)
                        .build()
                )
                .setVersion(version)
                .build();
        nettySocketServer.sendData(serialNumber, instruction, null);
    }
}
